package org.hermes.core;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.hermes.core.exception.AuthenticationRequiredException;
import org.hermes.core.message.ServiceMessage;
import org.hermes.core.security.Authentication;
import org.hermes.core.util.StringUtils;
import org.hermes.core.util.XMLUtil;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;

import com.google.gson.Gson;

public class HttpServiceBroker extends HttpServlet {
	private static final String SERVICE_FILE_PATH = "/WEB-INF/resources/hermes-services.xml";
	private static Map<String, Service> mappedServices;
	
	private AuthenticationVerify authVerify;
	
	static {
		mappedServices = new HashMap<String, Service>();
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		doPost(req, resp);
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		try {
			HttpSession session = req.getSession();
			
			ServiceResponse serviceResponse = new ServiceResponse();
			String serviceRequest = getServiceRequest(req);
			
			int indexSeparator = serviceRequest.indexOf(".");

			if (indexSeparator == -1) {
				throw new IllegalStateException("Method name is required to call service. Verify the sintaxe of call. Sample: Service.methodName");
			}

			String serviceName = serviceRequest.substring(0, indexSeparator);
			String methodName = serviceRequest.substring(indexSeparator + 1);

			Service service = mappedServices.get(serviceName);

			if (service == null) {
				throw new IllegalStateException("Service \""+ serviceName +"\" not found.");
			}

			if(service.authRequired){
				if(authVerify.authentication == null){
					throw new UnsupportedOperationException("Well, the service \""+ serviceName +"\" was checked how \"authenticatio=required\", but the hermes don't have class to authentication.");
				}
				
				if(authVerify.authentication.isAuthenticated(session)){
					if(authVerify.redirectTo != null){
						resp.sendRedirect(authVerify.redirectTo);
					}else{
						throw new AuthenticationRequiredException("Authentication required.");
					}
				}
			}
			
			Method serviceMethod = getMethodAtService(methodName, service.serviceInstance);
			
			Object requestData = null;
			
			if(serviceMethod.getParameterTypes().length > 0){
				ServletInputStream in = req.getInputStream();
				BufferedReader buffReader = new BufferedReader(new InputStreamReader(in));
				
				StringBuffer buffData = new StringBuffer();
				String line = null;
				
				while((line = buffReader.readLine()) != null){
					buffData.append(line);
				}
				
				if(buffData.length() > 0){
					requestData = new Gson().fromJson(buffData.toString(), serviceMethod.getParameterTypes()[0]);
				}
			}
			
			ServiceContext.newInstance(req);
			
			try{
				if(serviceMethod.getParameterTypes().length > 0){
					serviceResponse.setResponseData(serviceMethod.invoke(service.serviceInstance, requestData));
				}else{
					serviceResponse.setResponseData(serviceMethod.invoke(service.serviceInstance));
				}
				
				serviceResponse.setServiceMessages(ServiceContext.getCurrentServiceContext().getServiceMessages());
				serviceResponse.setStatus(ServiceResponse.STATUS_OK);
			}catch(Exception e){
				String msg = e.getCause().getMessage();
					
				if(e.getCause() instanceof NullPointerException){
					msg = "Internal application error (NPE)";
				}
				
				serviceResponse.addServiceMessage(new ServiceMessage(msg, ServiceMessage.TYPE_ERROR));
				serviceResponse.setStatus(ServiceResponse.STATUS_EXCEPTION);
				
				e.printStackTrace();
			}
			
			ServiceContext.destroy();
			
			resp.setContentType("text/plain");
			deliveryResponse(serviceResponse, resp);
		}catch(AuthenticationRequiredException ae){
			deliveryResponse(new ServiceResponse(ServiceResponse.STATUS_EXCEPTION, ae.getMessage()), resp);
			ae.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private void deliveryResponse(ServiceResponse serviceResponse, HttpServletResponse resp) throws IOException{
		Gson gson = new Gson();
		String strResponse = gson.toJson(serviceResponse);
		ServletOutputStream out = resp.getOutputStream();
		out.print(strResponse);
		out.flush();
	}

	private Method getMethodAtService(String methodName, Object service) throws Exception{
		Method[] methods = service.getClass().getDeclaredMethods();
		
		Method foundMethod = null;
		
		for(Method method : methods){
			if(methodName.equals(method.getName())){
				if(foundMethod == null){
					foundMethod = method;
				//Priority to less parameters.
				}else if(foundMethod.getParameterTypes().length < method.getParameterTypes().length){
					foundMethod = method;
				}
			}
		}
		
		if(foundMethod == null){
			throw new NoSuchMethodException("Method \"" + methodName + "\" not found. Did you create this method in your service?");
		}
		
		if(foundMethod.getParameterTypes().length > 1){
			throw new UnsupportedOperationException("Unsupported method with more that one parameter.");
		}
		
		return foundMethod;
	}
	
	private String getServiceRequest(HttpServletRequest req) {
		String servletPath = req.getServletPath();
		int start = servletPath.indexOf("/") + 1;
		int end = servletPath.indexOf(".sbr");

		return servletPath.substring(start, end);
	}

	@Override
	public void init(ServletConfig config) throws ServletException {
		try {
			System.out.println("[HERMES] Initializing services...");
			
			SAXBuilder sb = new SAXBuilder();

			InputStream serviceFile = config.getServletContext().getResourceAsStream(SERVICE_FILE_PATH);

			if (serviceFile == null) {
				throw new IOException("File not found.");
			}

			Document doc = sb.build(serviceFile);
			Element hermes = doc.getRootElement();
			
			Element authElem = hermes.getChild("authentication");
			
			if(authElem != null){
				authVerify = new AuthenticationVerify();
				
				Element authClassElem = authElem.getChild("class");
				
				if(authClassElem != null){
					String authClassName = StringUtils.getEmptyAsNull(authClassElem.getValue());
					try{
						authVerify.authentication = (Authentication) Class.forName(authClassName).newInstance();
					}catch(Exception e){
						System.out.println("[HERMES] [ERROR]Erro loading Authentication class: " + authClassName);
						System.out.println("[HERMES] [ERROR]It exists and it's correct? Please, verify the hermes-services.xml");
						e.printStackTrace();
					}
				}
				
				Element redirectElem = authElem.getChild("redirect");
				
				if(redirectElem != null){
					String redirectTo = StringUtils.getEmptyAsNull(redirectElem.getValue());
					authVerify.redirectTo = redirectTo;
				}
			}
			
			Element servicesElmen = hermes.getChild("services");
			
			if(servicesElmen != null){
				List<Element> servicesList = servicesElmen.getChildren("service");
				
				if(servicesList.isEmpty()){
					String errorMsg ="Not found services mappeds in xml config, the content of <services>";
					System.out.println("[ERROR HERMES] " +  errorMsg);
					throw new IllegalStateException(errorMsg);
				}
	
				for (Element serviceElem : servicesList) {
					String serviceName = XMLUtil.getRequiredAttributeAsString("name", serviceElem);
					String serviceClass = XMLUtil.getRequiredAttributeAsString("class", serviceElem);
					boolean authenticationRequired = "required".equalsIgnoreCase(XMLUtil.getAttributeAsString("authentication", serviceElem));
	
					Service service = new Service();
					service.serviceInstance = Class.forName(serviceClass).newInstance();
					service.authRequired = authenticationRequired;
					
					mappedServices.put(serviceName, service);
	
					System.out.println("[HERMES] Service \"" + serviceName + "\" was initialized.");
				}
			}else{
				throw new IllegalStateException("Services elment not found in configurations xml (file: " + SERVICE_FILE_PATH + ")");
			}
		} catch (IOException io) {
			throw new ServletException("[ERROR HERMES] The hemers's configuration file not found in " + SERVICE_FILE_PATH + ", did you create this file?", io);
		} catch (Exception e) {
			throw new ServletException("[ERROR HERMES] The hermes found a error and he can't continue.", e);
		}
	}
	
	private class AuthenticationVerify{
		Authentication authentication;
		String redirectTo;
	}
	
	private class Service{
		Object serviceInstance;
		boolean authRequired;
	}
}